#include    "core/errorpage.h"
#include    "core/router.h"
#include    "core/request.h"
#include    "core/response.h"
#include    "core/session.h"
#include    "core/storage.h"
#include    "core/scheduler.h"

#include    "utils/ini.h"
#include    "utils/logger.h"
#include    "utils/datetime.h"
#include    "utils/os.h"
#include    <microhttpd.h>
#include    <map>

#if defined(_WIN32)
#   include <Windows.h>
#   if defined(DEBUG) || defined(_DEBUG)
#       include <dbghelp.h>
#       pragma  comment(lib, "dbghelp")
#   endif
#   define  SLEEP(n)    Sleep(n)
#else
#	include	<unistd.h>
#   include <sys/resource.h>
#   define  SLEEP(n)    usleep(n * 1000)
#endif

static Ini config;
static std::string script_entry = "scripts/app.lua";
static std::map<std::string, std::string> shared_dirs;
extern std::string resty_template_engine;

/** Create lua VM */
static lua_State * CreateVM() {
    lua_State * lua = luaL_newstate();
    luaL_openlibs(lua);

    /// Open apis.
    extern void RegisterApi_OS(lua_State *);
    extern void RegisterApi_Json(lua_State *);
    extern void RegisterApi_Logger(lua_State *);
    extern void RegisterApi_Utils(lua_State *);
    extern void RegisterApi_Storage(lua_State *);
    extern void RegisterApi_Router(lua_State *);
    extern void RegisterApi_Response(lua_State *);
    extern void RegisterApi_Scheduler(lua_State *);
    extern void RegisterApi_XML(lua_State *);
    RegisterApi_OS(lua);
    RegisterApi_Json(lua);
    RegisterApi_Logger(lua);
    RegisterApi_Utils(lua);
    RegisterApi_Storage(lua);
    RegisterApi_Router(lua);
    RegisterApi_Response(lua);
    RegisterApi_Scheduler(lua);
    RegisterApi_XML(lua);

    /// Prepare config
    config.Push2Lua(lua);
    lua_setglobal(lua, "config");

    return lua;
}

/** Try to process request as shared file. */
static bool TryServeFile(struct MHD_Connection * conn, const std::string& url) {

    if (url == "/favicon.ico") {
        Response(conn).File("favicon.ico");
        return true;
    }

    std::string folder = "";
    std::string path = "";
    size_t pos = url.find_first_of('/', 1);
    if (pos == std::string::npos) {
        // Logger::Instance().Info("return false");
        // return false;
    } else {
        folder = url.substr(1, pos - 1);
        if (pos < url.size() - 1) {
            path = url.substr(pos + 1);
        }
    }
    
    for (auto& kv : shared_dirs) {
        auto matchUrl = kv.first;
        auto dict = kv.second; 

        if(matchUrl == "") {
            if (url.empty()) {
                Response forbidden(conn);
                forbidden.Error(MHD_HTTP_FORBIDDEN);
                forbidden.Flush();
            } else {
                int exist = Response(conn).FileExist(dict + "/" + url);
                if(exist == 1) {
                    Response(conn).File(dict + "/" + url);
                } else {
                    return false;
                }
            }

        } else if (matchUrl == folder && matchUrl != "") {
            if (path.empty()) {

                Response forbidden(conn);
                forbidden.Error(MHD_HTTP_FORBIDDEN);
                forbidden.Flush();
            } else {
                Response(conn).File(dict + "/" + path);
            }

            return true;
        }
    }

    return false;
}

/**
 * Callbacks for libmicrohttpd.
 */
namespace {

    int OnRequest(void * cls,
            struct MHD_Connection * conn,
            const char * request_url,
            const char * request_method,
            const char * version,
            const char * upload_data,
            size_t * upload_data_size,
            void ** conn_data) {
        double start_time = Tick();
        std::string url(request_url);
        std::string method(request_method);
    
        if (TryServeFile(conn, url)) {
            Logger::Instance().Verbose("%s\t%.3lfms\t%s", request_method, Tick() - start_time, request_url);
            return MHD_YES;
        }

        lua_State * lua = CreateVM();

        /// Load template engine.
        luaL_dostring(lua, resty_template_engine.c_str());
        /// Enable to show modified lua resty template engine.
    #if 0
        FILE * template_file = fopen("template.lua", "w+");
        if (template_file) {
            fwrite(resty_template_engine.c_str(), 1, resty_template_engine.size(), template_file);
            fflush(template_file);
            fclose(template_file);
        }
    #endif

        Router router;
        router.Prepare(lua);

        int top = lua_gettop(lua);
        lua_getglobal(lua, "debug");
        lua_getfield(lua, -1, "traceback");
        lua_remove(lua, -2);

        if (0 != luaL_loadfile(lua, script_entry.c_str()) || 0 != lua_pcall(lua, 0, LUA_MULTRET, top + 1)) {
            Logger::Instance().Error("Load entry script failed : %s.\n Setting main.script_entry in omni.ini.", lua_tostring(lua, -1));
            Response missing_rsp(conn);
            missing_rsp.Error(MHD_HTTP_INTERNAL_SERVER_ERROR);
            missing_rsp.Flush();
            Logger::Instance().Verbose("%s\t%.3lfms\t%s", request_method, Tick() - start_time, request_url);
            lua_close(lua);
            return MHD_YES;
        }

        lua_settop(lua, top + 1);
        
        std::vector<std::string> matches;
        int proc_ref = 0;
        int code = router.Check(url, method, proc_ref, matches);
        if (code != MHD_HTTP_OK) {
            Response err_rsp(conn);
            err_rsp.Error(code);
            err_rsp.Flush();
            Logger::Instance().Verbose("%s\t%.3lfms\t%s", request_method, Tick() - start_time, request_url);
            lua_close(lua);
            return MHD_YES;
        }

        lua_rawgeti(lua, LUA_REGISTRYINDEX, proc_ref);
        
        if (!Request::Prepare(conn, lua, start_time, url, method, upload_data, upload_data_size, conn_data)) {
            lua_close(lua);
            return MHD_YES;
        }

        Response rsp(conn);
        rsp.Prepare(lua);

        for (size_t i = 0; i < matches.size(); ++i) lua_pushstring(lua, matches[i].c_str());

        {
            Session session(lua, conn, &rsp);
            if (lua_pcall(lua, 2 + matches.size(), 0, top + 1) != 0) {
                Logger::Instance().Error("Dispatch error : %s\n", lua_tostring(lua, -1));
                rsp.Error(500);
            }
        }
        
        rsp.Flush();
        Logger::Instance().Verbose("%s\t%.3lfms\t%s", request_method, Tick() - start_time, request_url);
        lua_close(lua);
        return MHD_YES;
    }

    int OnComplete(void *, MHD_Connection * conn, void ** conn_data, MHD_RequestTerminationCode) {
        Request::PostData * p = (Request::PostData *)(*conn_data);
        *conn_data = NULL;
        if (p) {
            if (p->pp) MHD_destroy_post_processor(p->pp);
            for (auto & kv : p->uploaded) std::remove(kv.second.c_str());
            delete p;
        }
        return MHD_YES;
    }

    void OnLog(void *, const char * fmt, va_list args) {
        Logger::Instance().VerboseV(fmt, args);
    }

}

int main() {
    int port = 8080, expire = 900000;
    bool is_verbose = false, use_session = true;

    if (!config.Load("./omni.ini")) {
        Logger::Instance().Warn("Load configure ./omni.ini failed : %s. Using default setting!", config.GetError().c_str());
    } else {
        port = std::stoi(config.Get("main", "port", "8080"));
        expire = std::stoi(config.Get("main", "expire", "900000"));
        is_verbose = config.Get("main", "verbose", "no") == "yes";
        use_session = config.Get("main", "use_session", "no") == "yes";
        script_entry = config.Get("main", "script_entry", "scripts/app.lua");

        std::vector<std::string> keys = config.GetKeys("shared_dirs");
        for (auto& key : keys) {
            std::string dir = config.Get("shared_dirs", key, "");
            if (!dir.empty()) {
                if (dir[dir.size() - 1] == '/') dir = dir.substr(0, dir.size() - 1);
                if (!dir.empty()) {
                    if(key == "/") {
                        key = "";
                    }
                    Logger::Instance().Info("Load share_dirs[%s] = %s", key, dir);
                    shared_dirs[key] = dir;
                }
            }
        }

        auto errs = config.GetKeys("error_pages");
        for (auto err_no : errs) ErrorPage::Instance().Set(std::stoi(err_no), config.Get("error_pages", err_no, ""));
    }

    Storage::Instance().SetExpire(expire);
    Session::Init(use_session);

    MHD_Daemon *server = MHD_start_daemon(
        MHD_USE_AUTO_INTERNAL_THREAD | MHD_USE_ERROR_LOG,
        (uint16_t)port,
        NULL, NULL,
        &OnRequest, NULL,
        MHD_OPTION_CONNECTION_TIMEOUT, (unsigned int)120,
        MHD_OPTION_NOTIFY_COMPLETED, &OnComplete, NULL,
        MHD_OPTION_EXTERNAL_LOGGER, &OnLog, NULL,
        MHD_OPTION_END);

    if (!server) {
        Logger::Instance().Error("Failed to start HTTP service.");
        return 0;
    }

    /// Enable core dump for debug
#if defined(_WIN32)
    ::SetConsoleOutputCP(65001);
#   if (defined(_DEBUG) || defined(DEBUG))
    ::SetUnhandledExceptionFilter([](struct _EXCEPTION_POINTERS * exceptions) -> LONG {
        char file_name[64] = {0};
        DateTime now;

        snprintf(file_name, 64, "omni_crash_%04d-%02d-%02d_%02d%02d%02d.dmp",
            now.year, now.month, now.day, now.hour, now.minute, now.second);

        HANDLE dump = CreateFileA(file_name, GENERIC_WRITE, FILE_SHARE_WRITE, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
        if (dump != INVALID_HANDLE_VALUE) {
            MINIDUMP_EXCEPTION_INFORMATION info;
            info.ThreadId = GetCurrentThreadId();
            info.ExceptionPointers = exceptions;
            info.ClientPointers = NULL;
            MiniDumpWriteDump(GetCurrentProcess(), GetCurrentProcessId(), dump, MiniDumpWithFullMemory, &info, NULL, NULL);
            CloseHandle(dump);
        }
        
        return EXCEPTION_EXECUTE_HANDLER;
    });
#   endif
#else
    struct rlimit core;
    getrlimit(RLIMIT_CORE, &core);
    core.rlim_cur = RLIM_INFINITY;
    core.rlim_max = RLIM_INFINITY;
    setrlimit(RLIMIT_CORE, &core);
#endif

    /// Verbose log setting.
    if (is_verbose) Logger::Instance().EnableVerbose();
    Logger::Instance().Info("Started server on [:%d]", port);

    std::string scheduler_script = config.Get("main", "scheduler_script", "");
    if (!scheduler_script.empty() && Exists(scheduler_script.c_str())) {
        lua_State * lua = CreateVM();

        int top = lua_gettop(lua);
        lua_getglobal(lua, "debug");
        lua_getfield(lua, -1, "traceback");
        lua_remove(lua, -2);

        if (0 != luaL_loadfile(lua, scheduler_script.c_str()) || 0 != lua_pcall(lua, 0, LUA_MULTRET, top + 1)) {
            Logger::Instance().Error("Load %s for scheduler failed : %s", scheduler_script.c_str(), lua_tostring(lua, -1));
            lua_close(lua);
            while (true) SLEEP(6000);
        } else {
            lua_settop(lua, top + 1);
            while (true) {
                Scheduler::Instance().Breath(lua);
                SLEEP(50);
            }
            lua_close(lua);
        }        
    } else {
        while (true) SLEEP(6000);
    }
    
    MHD_stop_daemon(server);    
    return 0;
}